/*
 * HtmlFile.java
 *
 *  created: 1.9.2011
 *  charset: UTF-8
 *  license: MIT (X11) (See LICENSE file for full license)
 */
package cz.mp.k3bg.core;

import cz.mp.k3bg.log.LoggerManager;
import cz.mp.util.FileUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTML.Tag;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.HTMLEditorKit.ParserCallback;
import static cz.mp.k3bg.Application.EOL;

/**
 * Třída {@code HtmlFile} představuje HTML nebo XHTML soubor.
 * Třída parsuje (X)HTML a získává z něho seznam obrázků a stylů,
 * zjistí kódování a testuje zda je soubor XHTML.
 * (viz {@linkplain #initExisting()}).
 * 
 * @author Martin Pokorný
 * @version 0.1
 * @see ItemFile
 * @see SwingHtmlParser
 */
public class HtmlFile extends ItemFile {

    private static final boolean DEBUG = false;
    private static final Logger logger =
            LoggerManager.getLogger(HtmlFile.class, DEBUG);
    
    /** Seznam všech obrázků v tomto souboru. */
    private Set<ItemImage> images = new TreeSet<ItemImage>();

    /** Seznam všech souborů definic stylů v tomto souboru. */
    private Set<StyleFile> styleFiles = new TreeSet<StyleFile>();
    
    /** Opis všech vnitřních definic stylů (tag {@literal <style>}) 
     * v tomto souboru. */
    private ArrayList<String> internalStyles = new ArrayList<String>();
    
    /** Seznam všech odkazů v tomto souboru. */
    private ArrayList<Reference> references = new ArrayList<Reference>();
        
    /** */
    private boolean initialized = false;
    
    /** 
     * Příznak říkající zda se mají odkazy na styly a obrázky převést na
     * absolutní cesty. 
     */
    private boolean convertImgAndStylePathToAbs = false;
    
    // -----
    
    /**
     * 
     * @param path 
     */
    public HtmlFile(String path) {
        super(path);
    }
    
    // -----
    
    
    /**
     * Inicializuje HTML soubor, provede: zjištění kódování, získání
     * seznamu obrázků a stylů, testuje zda jde o XHTML.
     * 
     * @throws IOException 
     */
    public boolean initExisting() throws IOException {
        return initExistingImpl();
    }
    
    /**
     * 
     * @throws IOException 
     */
    private boolean initExistingImpl() throws IOException {
        logger.fine("path = " + path + "   exists = " + exists() 
                + "   initialized = " + initialized);
        if (! exists()) {
            logger.fine("return false");
            return false;
        }

        mimeType = MimeType.HTML;
        checkXmlTag();
        parseHtmlInfo();
        initialized = true;
        logger.fine("initialized = " + initialized);
        return true;
    }

    /**
     * 
     * @return 
     */
    public boolean isInitialized() {
        return initialized;
    }    
    
    
    /**
     * Testuje zda soubor začíná XML hlavičkou; pokud ano,
     * upraví podle toho mime typ a kódování.
     * 
     * @throws IOException 
     */
    private void checkXmlTag() throws IOException {
        logger.finer("");
        
        BufferedReader srcReader = getBufferedReader();

        // <?xml version="1.0" encoding="UTF-8"?>
        String line;
        while ((line = srcReader.readLine()) != null) {
            if (line.trim().length() > 0) {
                break;
            }
        }
            
        if (line != null && line.trim().toLowerCase().startsWith("<?xml")) {
            logger.finer("line = " + line);
            
            mimeType = MimeType.XHTML;

            Pattern pattern = Pattern.compile(
                    "encoding=\\s*[\"']?\\s*(.*?)\\s*[\"']?\\s*\\?", 
                    Pattern.CASE_INSENSITIVE);
            Matcher matcher = pattern.matcher(line);
            if (matcher.find()) {
                charset = Charset.forName(matcher.group(1));
            }
        }        

        srcReader.close();          
    }
    
    /**
     * 
     */
    private void initLists() {
        images = new TreeSet<ItemImage>();
        styleFiles = new TreeSet<StyleFile>();
        internalStyles = new ArrayList<String>();
        references = new ArrayList<Reference>();        
    }

    /**
     * 
     * @return
     * @throws IOException 
     */
    private void parseHtmlInfo() throws IOException {
        logger.fine("");
        
        initLists();
                
        BufferedReader srcReader = getBufferedReader();
                
        logger.finer("parse!");
        
        HTMLEditorKit.Parser parser = new SwingHtmlParser().getParser();
        HtmlInfoParserCallback callback = new HtmlInfoParserCallback();
        parser.parse(srcReader, callback, true);        
        
        srcReader.close();
        logger.fine("finished!");
    }    
    
    /**
     * Získá množinu obrázků, použitých v tomto (X)HTML souboru.
     * 
     * @return 
     */
    public Set<ItemImage> getImages() {
        if (initialized == false) {
            throw new IllegalStateException("not initialized");
        }
        if (images == null) {
            throw new IllegalStateException("images=null");
        }
        return Collections.unmodifiableSet(images);
    }
       
    /**
     * Získá množinu souborů stylů, použitých v tomto (X)HTML souboru.
     * 
     * @return 
     */
    public Set<StyleFile> getStyleFiles() {
        if (initialized == false) {
            throw new IllegalStateException("not initialized");
        }        
        if (styleFiles == null) {
            throw new IllegalStateException("styleFiles=null");
        }
        return Collections.unmodifiableSet(styleFiles);
    }
    
    /**
     * Získá opis vnitřních stylů definovaných v tomto (X)HTML souboru.
     * 
     * @return 
     */
    public ArrayList<String> getIntenalStyles() {
        if (initialized == false) {
            throw new IllegalStateException("not initialized");
        }
        if (internalStyles == null) {
            throw new IllegalStateException("internalStyles=null");
        }        
        return new ArrayList<String>(internalStyles);
    }
    
    /**
     * Získá seznam odkazů použitých v tomto (X)HTML souboru.
     * 
     * @return 
     */
    public ArrayList<Reference> getReferences() {
        if (initialized == false) {
            throw new IllegalStateException("not initialized");
        }
        if (references == null) {
            throw new IllegalStateException("references=null");
        }          
        return new ArrayList<Reference>(references);
    }

    
    
    /**
     * 
     * @return 
     */
    boolean isConvertImgAndStylePathToAbs() {
        return convertImgAndStylePathToAbs;
    }

    /**
     * 
     * @param convertImgAndStylePathToAbs 
     */
    public void setConvertImgAndStylePathToAbs(
            boolean convertImgAndStylePathToAbs) {
        logger.config("convertImgAndStylePathToAbs = "
                + convertImgAndStylePathToAbs);
        this.convertImgAndStylePathToAbs = convertImgAndStylePathToAbs;
    }
    
    // (pozn. název metody chce vlepšit)
    /**
     * 
     * @param destPath
     * @return 
     */
    private String updateReferenceToAbs(String destPath) {
        logger.finer("destPath = " + destPath);
        if (convertImgAndStylePathToAbs) {
            // http
            if (destPath.startsWith("http://")) {
                return destPath;                
            }
            // adresář
            try {
                String destPathAbs = 
                        FileUtils.absolutizePathToDest(getPath(), destPath);
                logger.finer(destPathAbs);
                return destPathAbs;
            } catch (IOException ex) {
                logger.warning(ex.toString());
                return destPath;
            }
        }
        return destPath;
    }

    @Override
    public boolean equals(Object obj) {
        return super.equals(obj);
    }

    @Override
    public int hashCode() {
        return super.hashCode();
    }    
    
    // -------------------------------------------------------------------------

    /**
     * Parsuje HTML soubor, získává seznam obrázků, stylů, seznam opisů
     * vnitřních stylů, získává kódování znaků.
     * 
     * @see ParserCallback
     * @see HTMLEditorKit
     */
    private class HtmlInfoParserCallback extends ParserCallback {


        private boolean inTagStyle = false;
        
        private boolean inTagA = false;
        private String lastAhref = "";
        private String lastAvalue = "";
        
        public HtmlInfoParserCallback() {
            super();
            logger.fine("");
        }

        @Override
        public void handleStartTag(Tag tag, 
                MutableAttributeSet attributes, int pos) {
            if (tag == Tag.STYLE) {
                inTagStyle = true;
            }
            if (tag == Tag.A) {
                inTagA = true;
                
                Object hrefObj = attributes.getAttribute(HTML.Attribute.HREF);
                if (hrefObj != null) {
                    lastAhref = hrefObj.toString();
                }
            }            
        }

        @Override
        public void handleEndTag(Tag tag, int pos) {
            if (tag == Tag.STYLE) {
                inTagStyle = false;
            }
            if (tag == Tag.A) {
                if (lastAhref != null && ! lastAhref.isEmpty()) {
                    Reference reference = new Reference(lastAhref, lastAvalue);
                    references.add(reference);
                }
                inTagA = false;
                lastAhref = "";
                lastAvalue = "";
            }
        }

        @Override
        public void handleText(char[] data, int pos) {
            if (inTagStyle) {
                String style = new String(data);
                
                String[] sss = style.split("}");                
                StringBuilder sb = new StringBuilder();
                sb.append(EOL);
                for (int i=0; i<sss.length-1; i++) {
                    sb.append(sss[i].trim()).append(" }");
                    sb.append(EOL);
                }
                
                internalStyles.add(sb.toString());
            }
            
            if (inTagA) {
                lastAvalue = new String(data);
            }
        }
        
        @Override
        public void handleSimpleTag(Tag tag, 
                MutableAttributeSet attributes, int pos) {

            if (tag == Tag.META) {
                Object httpequivObj = 
                        attributes.getAttribute(HTML.Attribute.HTTPEQUIV);
                Object contentObj = 
                        attributes.getAttribute(HTML.Attribute.CONTENT);
                if (httpequivObj != null && 
                        httpequivObj.toString()
                            .equalsIgnoreCase("content-type")) {
                    if (contentObj != null) {
                        String content = contentObj.toString();
                        String[] temp = content.split("charset=");
                        if (temp.length == 2) {
                            charset = Charset.forName(temp[1].trim());
                        }
                    }
                }
            }   
                
            if (tag == Tag.LINK) {
                Object hrefObj = attributes.getAttribute(HTML.Attribute.HREF);
                // stylesheet
                Object relObj = attributes.getAttribute(HTML.Attribute.REL);
//                // text/css
//                Object typeObj = attributes.getAttribute(HTML.Attribute.TYPE);

                if (relObj != null && 
                        relObj.toString().equalsIgnoreCase("stylesheet")) {
                    if (hrefObj != null) {
                        String hrefText = 
//                                hrefObj.toString();
                                updateReferenceToAbs(hrefObj.toString());
                        styleFiles.add(new StyleFile(hrefText));
                    }
                }
            }

            if (tag == Tag.IMG) {
                Object srcObj = attributes.getAttribute(HTML.Attribute.SRC);
//                Object altObj = attributes.getAttribute(HTML.Attribute.ALT);
                Object wObj = attributes.getAttribute(HTML.Attribute.WIDTH);
                Object hObj = attributes.getAttribute(HTML.Attribute.HEIGHT);

                if (srcObj == null) {
                    return;
                }
                String srcText = 
//                        srcObj.toString();
                        updateReferenceToAbs(srcObj.toString());
//                String altText = altObj == null ? null : altObj.toString();
                String widthText = wObj == null ? null : wObj.toString();
                String heightText = hObj == null ? null : hObj.toString();

                if (widthText == null || heightText == null) {
                    images.add(new ItemImage(srcText));
                }
                else {
                    try {
                        int width = Integer.valueOf(widthText);
                        int height = Integer.valueOf(heightText);
                        images.add(new ItemImage(srcText, width, height));
                    } catch (NumberFormatException ex) {
                        images.add(new ItemImage(srcText));
                    }                
                }
            }
        }
        
    }   // HtmlInfoParserCallback     
    
}   // HtmlFile.java

